﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using ACorns.VSTools.VSSpeeder.BuildRunner;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
using System.Diagnostics;
using EnvDTE;
using VSSpeeder.Build;
using VSSpeeder.Properties;
using System.Reflection;
using System.Windows.Forms;
using Microsoft.VisualStudio.CommandBars;

namespace VSSpeeder
{
	public class SolutionEventsListener : IVsUpdateSolutionEvents2, IVsUpdateSolutionEvents, IVsSolutionEvents
	{
		private Stopwatch _buildStopWatch;

		public Connect _connect;
		private OutputWindow _outputWindow;

		private OutputWindowPane _buildWindow;
		private OutputWindowPane _reducedBuildWindow;
		private bool _forceStopBuildOperation = false;
		private MSBuildRunner _runner;

		public bool Building { get; private set; }

		public VSSpeeder.Utils.ErrorProvider ErrorProvider { get; private set; }

		private List<CommandEvents> _reroutedCommands = new List<CommandEvents>();
		private CommandBarButton _buildCancelCommand;
		private string _commandToRerun;
		
		private bool _rerunningCommand;
		private string _rerunOperationBuildOutput;	// VS Clears the Build panel if we cancel an operation so we have to "rebuild" it :(
		private int _totalProjects;
		private bool _allowDefaultOperation = false;

		public SolutionEventsListener(Connect connect)
		{
			_connect = connect;
			var window = _connect.ApplicationObject.Windows.Item(EnvDTE.Constants.vsWindowKindOutput);
			_outputWindow = window.Object as OutputWindow;

			// find the build window pane
			for (int i = 1; i < _outputWindow.OutputWindowPanes.Count; i++)
			{
				OutputWindowPane pane = _outputWindow.OutputWindowPanes.Item(i);

				if (pane.Name == "Build")
				{
					_buildWindow = pane;
					_buildWindow.Activate();	// make sure it's created
					break;
				}
			}
			//_reducedBuildWindow = _outputWindow.OutputWindowPanes.Add("Reduced Build");
			ErrorProvider = new VSSpeeder.Utils.ErrorProvider(this);

			// Debug.Start command...
			var debugCommand = _connect.ApplicationObject.Events.get_CommandEvents("{5EFC7975-14BC-11CF-9B2B-00AA00573819}", (int)Microsoft.VisualStudio.VSConstants.VSStd97CmdID.Start);
			debugCommand.BeforeExecute += new _dispCommandEvents_BeforeExecuteEventHandler(_debugCommand_BeforeExecute);
			_reroutedCommands.Add(debugCommand);

			var startCommand = _connect.ApplicationObject.Events.get_CommandEvents("{5EFC7975-14BC-11CF-9B2B-00AA00573819}", (int)Microsoft.VisualStudio.VSConstants.VSStd97CmdID.StartNoDebug);
			startCommand.BeforeExecute += new _dispCommandEvents_BeforeExecuteEventHandler(_startCommand_BeforeExecute);
			_reroutedCommands.Add(startCommand);

			var cleanSolution = _connect.ApplicationObject.Events.get_CommandEvents("{5EFC7975-14BC-11CF-9B2B-00AA00573819}", (int)Microsoft.VisualStudio.VSConstants.VSStd97CmdID.CleanSln);
			cleanSolution.BeforeExecute += new _dispCommandEvents_BeforeExecuteEventHandler(_clean_BeforeExecute);
			_reroutedCommands.Add(cleanSolution);

			var cleanCommand = _connect.ApplicationObject.Events.get_CommandEvents("{5EFC7975-14BC-11CF-9B2B-00AA00573819}", (int)Microsoft.VisualStudio.VSConstants.VSStd97CmdID.CleanSel);
			cleanCommand.BeforeExecute += new _dispCommandEvents_BeforeExecuteEventHandler(_clean_BeforeExecute);
			_reroutedCommands.Add(cleanCommand);

			var cleanProj = _connect.ApplicationObject.Events.get_CommandEvents("{5EFC7975-14BC-11CF-9B2B-00AA00573819}", (int)Microsoft.VisualStudio.VSConstants.VSStd97CmdID.CleanProjPicker);
			cleanProj.BeforeExecute += new _dispCommandEvents_BeforeExecuteEventHandler(_clean_BeforeExecute);
			_reroutedCommands.Add(cleanProj);

			var cleanCtx = _connect.ApplicationObject.Events.get_CommandEvents("{5EFC7975-14BC-11CF-9B2B-00AA00573819}", (int)Microsoft.VisualStudio.VSConstants.VSStd97CmdID.CleanCtx);
			cleanCtx.BeforeExecute += new _dispCommandEvents_BeforeExecuteEventHandler(_clean_BeforeExecute);
			_reroutedCommands.Add(cleanCtx);

			// ReBuild Commands
			var rebuildProject = _connect.ApplicationObject.Events.get_CommandEvents("{5EFC7975-14BC-11CF-9B2B-00AA00573819}", (int)Microsoft.VisualStudio.VSConstants.VSStd97CmdID.RebuildProjPicker);
			rebuildProject.BeforeExecute += new _dispCommandEvents_BeforeExecuteEventHandler(rebuildProject_BeforeExecute);
			_reroutedCommands.Add(rebuildProject);
		}

		#region Command Hooks
		void rebuildProject_BeforeExecute(string Guid, int ID, object CustomIn, object CustomOut, ref bool CancelDefault)
		{
			throw new NotImplementedException();
		}
		void _debugCommand_BeforeExecute(string Guid, int ID, object CustomIn, object CustomOut, ref bool CancelDefault)
		{
			_commandToRerun = "Debug.Start";
		}
		void _startCommand_BeforeExecute(string Guid, int ID, object CustomIn, object CustomOut, ref bool CancelDefault)
		{
			_commandToRerun = "Debug.StartWithoutDebugging";
		}
		void _clean_BeforeExecute(string Guid, int ID, object CustomIn, object CustomOut, ref bool CancelDefault)
		{
			_allowDefaultOperation = true;
		}
		#endregion

		public void KillBuild()
		{
			if (_runner != null)
			{
				_runner.KillBuild();
			}
			Building = false;
		}

		public int TotalNumberOfProjects
		{
			get
			{
				//var projects = _connect.ApplicationObject.ActiveSolutionProjects as ActiveSo;
				var solution = _connect.VSServiceProvider.GetService(typeof(SVsSolution)) as IVsSolution2;
				if (solution != null)
				{
					// Get count of any currently loaded projects
					object count;
					solution.GetProperty((int)__VSPROPID.VSPROPID_ProjectCount, out count);
					return (int)count;
				}
				return 0;
			}
		}
		public int OnActiveProjectCfgChange(IVsHierarchy pIVsHierarchy)
		{
			return 0;
		}

		public int UpdateProjectCfg_Begin(IVsHierarchy pHierProj, IVsCfg pCfgProj, IVsCfg pCfgSln, uint dwAction, ref int pfCancel)
		{
			if (_forceStopBuildOperation)
				pfCancel = 1;	// cancelled
			return 0;
		}

		/// <summary>
		/// Project finished to build
		/// </summary>
		public int UpdateProjectCfg_Done(IVsHierarchy pHierProj, IVsCfg pCfgProj, IVsCfg pCfgSln, uint dwAction, int fSuccess, int fCancel)
		{
			if (fSuccess == 0 && Settings.Default.StopBuildOnFirstFailure)
			{
				//if (!_forceStopBuildOperation)
				//{
				//    _forceStopBuildOperation = true;
				//    _buildWindow.OutputString("\r\n");
				//    _buildWindow.OutputString("Project failed to build. Stopping the build process ...");
				//    _buildWindow.OutputString("\r\n");
				//    _buildWindow.OutputString("\r\n");

				//    //// We should try to read the last error and automatically open the file :)
				//    //var textDocument = _buildWindow.TextDocument;
				//    //var start = textDocument.CreateEditPoint();
				//    //var end = textDocument.CreateEditPoint();
				//    //start.StartOfDocument();
				//    //if (start.FindPattern("): error ", (int)vsFindOptions.vsFindOptionsFromStart))
				//    //{
				//    //    start.StartOfLine();
				//    //    var errorLine = start.GetLines(start.Line, start.Line + 1);

				//    //    var fileName = errorLine.UpTo("(");
				//    //    var errorLineNumber = Int32.Parse(errorLine.After("(").UpTo(","));
				//    //    var errorColNumber = Int32.Parse(errorLine.After(",").UpTo(")"));

				//    //    //if (System.IO.File.Exists(fileName))
				//    //    //{
				//    //    //    // try to open the file
				//    //    //    Window newFileWindow = _connect.ApplicationObject.ItemOperations.OpenFile(fileName);
				//    //    //    newFileWindow.Activate();

				//    //    //    TextDocument document = (TextDocument)newFileWindow.Document;
				//    //    //    EditPoint point = document.StartPoint.CreateEditPoint();
				//    //    //    point.MoveToLineAndOffset(errorLineNumber, errorColNumber);
				//    //    //    point.TryToShow(vsPaneShowHow.vsPaneShowCentered);
				//    //    //}
				//    //}
				//}
			}
			return 0;
		}

		public int UpdateSolution_Begin(ref int pfCancelUpdate)
		{
			if (_rerunningCommand)
			{
				return 0;
			}

			_rerunOperationBuildOutput = null;
			if (_allowDefaultOperation)
			{	// don't do parallel compilation
				_allowDefaultOperation = false;
				ErrorProvider.Clear();
				return 0;
			}

			if (_runner != null && _runner.Building)
			{
				pfCancelUpdate = 1;	// force cancel!
				_buildWindow.OutputString("Already building. Please stop the current build before starting a new build ...\r\n");
				return 0;
			}

			ErrorProvider.Clear();
			if (Settings.Default.ForceParallelBuilds)
			{
				// Save all
				_connect.ApplicationObject.DTE.ExecuteCommand("File.SaveAll", String.Empty);
				
				Building = true;
				_connect.CancelParallelBuilds.Refresh();

				//_connect.ApplicationObject.StatusBar.Progress()
				// stop the standard build we'll kick a separate build process in the background
				pfCancelUpdate = 1;
				// Kick a parallel build
#if DEBUG
				_buildWindow.Activate();
#endif
				_buildWindow.Clear();
				_buildWindow.OutputString("Starting parallel build ...\r\n");
				
				_buildStopWatch = new Stopwatch();
				_buildStopWatch.Start();

				// Get the solution Path
				// TODO: Maybe improve this to do partial builds!
				var solutionFileName = _connect.ApplicationObject.Solution.FileName;

				_totalProjects = TotalNumberOfProjects;
				_connect.ApplicationObject.StatusBar.Progress(true, "Parallel build ...", 1, 10);
				
			
				 // Try to detect solution configuration (Build/Release)
				var configuration = _connect.ApplicationObject.DTE.Solution.SolutionBuild.ActiveConfiguration;
				
				_runner = new MSBuildRunner(solutionFileName, _buildWindow, this);
				_runner.ParallelBuilds = true;
				_runner.SolutionConfiguration = configuration.Name;
				_runner.Start();
			}
			return 0;
		}

		public int UpdateSolution_Done(int fSucceeded, int fModified, int fCancelCommand)
		{
			CalculateAndDisplayBuildTime();
			if (_rerunOperationBuildOutput != null)
			{
				_buildWindow.OutputString(_rerunOperationBuildOutput);
				_rerunOperationBuildOutput = null;
			}
			return 0;
		}

		public int UpdateSolution_StartUpdate(ref int pfCancelUpdate)
		{
			_forceStopBuildOperation = false;	// reset
			_buildStopWatch = new Stopwatch();
			_buildStopWatch.Start();
			return 0;
		}

		#region IVsSolutionEvents
		public int OnAfterCloseSolution(object pUnkReserved)
		{
			ErrorProvider.Clear();
			return 0;
		}

		public int OnAfterLoadProject(IVsHierarchy pStubHierarchy, IVsHierarchy pRealHierarchy)
		{
			return 0;
		}

		public int OnAfterOpenProject(IVsHierarchy pHierarchy, int fAdded)
		{
			return 0;
		}

		public int OnAfterOpenSolution(object pUnkReserved, int fNewSolution)
		{
			StopDesigners();
			ErrorProvider.Clear();
			return 0;
		}

		public int OnBeforeCloseProject(IVsHierarchy pHierarchy, int fRemoved)
		{
			return 0;
		}

		public int OnBeforeCloseSolution(object pUnkReserved)
		{
			return 0;
		}

		public int OnBeforeUnloadProject(IVsHierarchy pRealHierarchy, IVsHierarchy pStubHierarchy)
		{
			return 0;
		}

		public int OnQueryCloseProject(IVsHierarchy pHierarchy, int fRemoving, ref int pfCancel)
		{
			return 0;
		}

		public int OnQueryCloseSolution(object pUnkReserved, ref int pfCancel)
		{
			return 0;
		}

		public int OnQueryUnloadProject(IVsHierarchy pRealHierarchy, ref int pfCancel)
		{
			return 0;
		}
		private void StopDesigners()
		{
			//if (Settings.Default.StopDesignersFromMonitoringTheBuild)
			//{
			//    Type objectsPackage = Type.GetType("Microsoft.VisualStudio.ORDesigner.DslPackage.ORDesignerObjectsPackage, Microsoft.VisualStudio.ORDesigner.DslPackage");

			//    var method = objectsPackage.GetMethod("UnregisterForSolutionEvents", BindingFlags.NonPublic | BindingFlags.Instance);

			//    var prop = objectsPackage.GetProperty("LoadedPackage", BindingFlags.NonPublic | BindingFlags.Static);
			//    var loadedPackage = prop.GetValue(null, null);

			//    if (loadedPackage == null)
			//    {
			//        //MessageBox.Show("Package not loaded. Try again after you open your solution.");
			//    }
			//    else
			//    {
			//        method.Invoke(loadedPackage, null);
			//        //MessageBox.Show("Unloaded some designers. VS should be slightly faster now.");
			//    }
			//}
		}
		#endregion
		#region Standard Console Processing
		public int UpdateSolution_Cancel()
		{
			CalculateAndDisplayBuildTime();
			return 0;
		}

		private void CalculateAndDisplayBuildTime()
		{
			//SimplifyBuildOutput();

			//if (_buildStopWatch != null)
			//{
			//    _buildStopWatch.Stop();
			//    if (_buildWindow != null && _buildStopWatch != null)
			//    {
			//        _buildWindow.OutputString(" ");
			//        _buildWindow.OutputString(CalculateBuildTime());
			//    }
			//}
		}

		private string CalculateBuildTime()
		{
			if (_buildStopWatch != null)
			{
				return "Build time:" + ((decimal)_buildStopWatch.ElapsedMilliseconds / (decimal)1000).ToString("###,##0.000") + "s";
			}
			return String.Empty;
		}

		private void SimplifyBuildOutput()
		{
			if (Settings.Default.SimplifyBuildOuput)
			{
				// Rebuild the Ouput Window Contents :) just cose we can
				TextDocument document = _buildWindow.TextDocument;
				var selection = document.Selection;
				selection.SelectAll();
				var lines = selection.Text.Split('\r');

				List<string> newLines = new List<string>();
				List<string> currentProjectLines = new List<string>();
				string currentProject = String.Empty;
				string warningsErrors = String.Empty;
				string outputLocation = String.Empty;
				bool projectBuildStopped = false;

				// process each line, rebuild the final output
				foreach (var xline in lines)
				{
					var line = xline.Trim();
					// try to detect different patterns in it
					if (line.Contains("): error "))
					{
						currentProjectLines.Add(line);
						continue;
					}
					if (line.StartsWith("Project failed to build. Stopping the build process"))
						projectBuildStopped = true;
					if (line.StartsWith("------ Build started: Project: ") || line.StartsWith("------ Rebuild All started: "))
					{
						// Save Last ProjectLines
						// read the project name!
						if (!String.IsNullOrEmpty(currentProject))
						{
							if (projectBuildStopped)
								warningsErrors = "- Build Stopped";
							DumpProjectDetails(newLines, currentProjectLines, currentProject + " " + warningsErrors + " -> " + outputLocation);
						}
						currentProject = line.After("Project: ").UpTo(",");
						outputLocation = String.Empty;
						warningsErrors = String.Empty;
					}
					if (line.StartsWith("Compile complete -- "))
					{
						warningsErrors = line.After("Compile complete -- ");
						//DumpProjectDetails(newLines, currentProjectLines, currentProject + " " + warningsErrors + " -> " + outputLocation);
					}
					if (!String.IsNullOrEmpty(currentProject) && line.Contains(currentProject + " -> "))
					{
						outputLocation = line.After(" -> ");
					}
				}
				DumpProjectDetails(newLines, currentProjectLines, currentProject + " " + warningsErrors + " -> " + outputLocation);
				//_reducedBuildWindow.Clear();
				//foreach (var line in newLines)
				//{	// rebuild them
				//    _reducedBuildWindow.OutputString(line + "\r\n");
				//}
				//_reducedBuildWindow.OutputString(" ");
				//_reducedBuildWindow.OutputString(CalculateBuildTime());
				//_reducedBuildWindow.Activate();
				//_reducedBuildWindow.TextDocument.Selection.StartOfDocument();
			}
		}

		private static void DumpProjectDetails(List<string> newLines, List<string> currentProjectLines, string currentProject)
		{
			// dump the project
			newLines.Add(">>> " + currentProject);
			foreach (var projLine in currentProjectLines)
				newLines.Add(projLine);
			newLines.Add("");
			currentProjectLines.Clear();
		}
		#endregion

		public void BuildFinished(bool hasErrors)
		{
			Building = false;
			// It's possible that this never hits the UI - thus we ned to kick the re-execute command from a new thread
			_connect.ApplicationObject.StatusBar.Progress(true, "Build complete ...", 10, 10);
			_buildStopWatch.Stop();
			_buildWindow.OutputString("Build finished. " + CalculateBuildTime());
			_connect.ApplicationObject.StatusBar.Progress(false, "Build complete ...", 10, 10);

			Trace.WriteLine("Build finished, progress bar updated ...");

            if (hasErrors)
            {
                ErrorProvider.ShowErrorWindow();
            }


			// See if we have to run some command);
			if (_commandToRerun != null)
			{
				//_rerunOperationBuildOutput
				TextDocument document = _buildWindow.TextDocument;
				var selection = document.Selection;
				selection.SelectAll(); // copy the output and regenerate it later
				_rerunOperationBuildOutput = selection.Text;
				_rerunOperationBuildOutput += "\r\nBuild finished. " + CalculateBuildTime();

				Trace.WriteLine("Queuing re-execution of: " + _commandToRerun);
				ThreadPool.QueueUserWorkItem(RerunLastCommand);
			}
		}

		private void RerunLastCommand(object state)
		{
			AsyncController.Instance.ExecuteOnUiThread((value) => RerunLastCommandOnUiThread());
		}
		private void RerunLastCommandOnUiThread()
		{
			try
			{
				Trace.WriteLine("Re-executing: " + _commandToRerun);
				_rerunningCommand = true;

				try
				{
					_connect.ApplicationObject.DTE.ExecuteCommand(_commandToRerun, String.Empty);
				}
				finally
				{
					_rerunningCommand = false;
					_commandToRerun = null;
				}
			}
			catch (Exception ex)
			{
				Trace.WriteLine("Exception wile re-running command: " + ex.Message);
			}
		}

		internal void ReportProgress(int processedProjects)
		{
			if (processedProjects == 0 || _totalProjects == 0)
				return;
			_connect.ApplicationObject.StatusBar.Progress(true, "Parallel build ...", processedProjects, _totalProjects);
		}
	}
}
